package math.vectorSpace.RX;

import math.mathExtended.DoubleComparator;
import math.mathExtended.MathExtended;
import math.vectorSpace.InnerProductSpace;

public abstract class Vector<V extends Vector<V>>
        implements InnerProductSpace<V> {

    private final double[] components;

    protected static final DoubleComparator comparator = new DoubleComparator();

    /**
     * Creates a vector from the given x0, x1, x2, ... xn double coordinates
     * 
     * @param components
     */
    public Vector(double[] components) {
        this.components = new double[components.length];
        System.arraycopy(components, 0, this.components, 0, components.length);
    }

    /**
     * Creates a vector from the components of the given vector.
     * 
     * @param v 
     */
    protected Vector(V v) {
        this.components = new double[v.getDimension()];
        for (int i = 0; i < v.getDimension(); i++) {
            this.components[i] = v.getComponent(i);
        }
    }

    /**
     * Returns the dimension of this vector.
     * 
     * @return the dimension of this vector
     */
    public final int getDimension() {
        return this.components.length;
    }

    /**
     * Returns the value of a component of this vector. Index 0 corresponds
     * to the x-value, index 1 corresponds to the y-value, index 2 corresponds
     * to the z-value, etc.
     * 
     * @param index the index of the component
     * @return the value of the component
     */
    protected final double getComponent(int index) {
        return components[index];
    }

    /**
     * Sets a component to the given value.
     * 
     * @param index the index of the component
     * @param value the new value of the component
     */
    protected final void setComponent(int index, double value) {
        this.components[index] = value;
    }

    /**
     * Finds the angle between this vector and the x,y,z, ... n - axis
     * 
     * @param index the index of the axis, 0 corresponds to the x-axis,
     * 1 corresponds to the y-axis, 2 corresponds to the z-axis, and so on.
     * @return the angle in degrees
     */
    protected final double angle(int index) {
        return MathExtended.aCosd(components[index] / length());
    }

    @Override
    public final double length() {
        return Math.sqrt(MathExtended.sumOfSquares(components));
    }

    /**
     * Find the direction of this vector
     * 
     * @return
     */
    public final V unitVector() {
        double[] newCoord = new double[components.length];
        for (int i = 0; i < components.length; i++) {
            newCoord[i] = components[i] / length();
        }
        return createVector(newCoord);
    }

    @Override
    public final V invert() {
        double[] newCoord = new double[components.length];
        for (int i = 0; i < components.length; i++) {
            newCoord[i] = -components[i];
        }
        return createVector(newCoord);
    }

    /**
     * 
     * @param vector
     * @return the angle in degrees [0, 180].
     */
    @Override
    public final double angleD(V vector) {
        double angle = MathExtended.aCosd((this.dot(vector))
                / (length() * vector.length()));
        if (angle > 180) {
            angle = 360 - angle;
        }
        return angle;
    }

    /**
     * 
     * @param vector
     * @return the angle in radians [0, pi]
     */
    @Override
    public final double angleR(V vector) {
        double angle = Math.acos(this.dot(vector))
                / (length() * vector.length());
        if (angle > Math.PI / 2) {
            angle = 2 * Math.PI - angle;
        }
        return angle;
    }

    @Override
    public final V scale(double scalar) {
        double[] newCoord = new double[components.length];
        for (int i = 0; i < components.length; i++) {
            newCoord[i] = components[i] * scalar;
        }
        return createVector(newCoord);
    }

    @Override
    public final V add(V vector) {
        double[] newCoord = new double[components.length];
        for (int i = 0; i < components.length; i++) {
            newCoord[i] = components[i] + vector.getComponent(i);
        }
        return createVector(newCoord);
    }

    /**
     * Returns the difference between this vector and the given vector
     * 
     * @param vector
     * @return
     */
    public final V subtract(V vector) {
        return this.add(vector.invert());
    }

    @Override
    public final double dot(V vector) {
        double sum = 0;
        for (int i = 0; i < components.length; i++) {
            sum += components[i] + vector.getComponent(i);
        }
        return sum;
    }

    /**
     * Returns the magnitude of the projection of this vector onto the 
     * given vector
     * 
     * @param v2
     * @return
     */
    public final double projectMag(V v2) {
        return this.dot(v2.unitVector());
    }

    @Override
    public final V projectOnto(V v2) {
        return v2.scale(this.dot(v2) / MathExtended.square(v2.length()));
    }

    @Override
    public final boolean isColinear(V vector) {
        double angle = this.angleD(vector);
        return (comparator.equal(angle, 0) || comparator.equal(angle, 180));
    }

    @Override
    public final boolean isEqual(V v2) {
        if (this.getDimension() != v2.getDimension()) {
            return false;
        }

        for (int i = 0; i < components.length; i++) {
            if (!comparator.equal(components[i], v2.getComponent(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if the given vector is orthogonal/perpendicular
     * to this vector.
     *
     * @param vector the other Vector
     * @return true if the two vectors are orthogonal, false otherwise
     */
    @Override
    public final boolean isOrthogonal(V vector) {
        return comparator.equal(this.dot(vector), 0);
    }

    /**
     * Returns a copy of this Vector.
     * 
     * @return 
     */
    public final V copy() {
        return createVector(this.components);
    }

    @Override
    public String toString() {
        if (components.length == 0) {
            return ("Vector []");
        }

        String temp = Double.toString(components[0]);
        for (int i = 1; i < components.length; i++) {
            temp += ", " + Double.toString(components[i]);
        }
        return ("Vector [" + temp + "]");
    }

    protected abstract V createVector(double[] components);
}
